输入一个网址后,DNS 到底做了什么?
一个被跳过的问题
上一次旅行结束时,数据包已经走完了一段很长的路——离开本地网络、穿过一台又一台路由器、找到服务器上的那个程序、裹上加密的外壳、钻进 Linux 内核,最后拿到了服务器的响应。
但走到最后,它回头看了一眼,发现一件事情不太对劲:
这一路上,它一直在使用一个地址——
203.0.113.10。
可这个地址是从哪来的,它从没弄明白过。它只记得,自己刚出发的时候手里拿的根本不是这个地址,而是一个名字:
www.example.com
名字变成地址的这一步,被悄悄跳过了。
这次,它决定回过头,把这一步真正走一遍。
名字和地址,是两回事
对人来说,www.example.com 很好记。但网络传输信息的时候,靠的从来不是名字,而是地址——就像寄信要写门牌号,不能只写”张先生家”。
所以在数据包能够出发之前,这台电脑必须先解决一个问题:
这个名字,到底对应着哪一个地址?
负责回答这个问题的,是 DNS。
这里要先纠正一个很容易产生的误解:DNS 不是”一台服务器”。它是一整套分布式的名称解析系统,由很多角色分工协作完成——没有哪一台机器单独”知道”全世界所有域名对应的地址。
第一步:先问自己身边的那个人
当浏览器需要知道 www.example.com 对应的地址时,它并不会自己跑去问遍全世界。它会把这个问题,交给一个通常配置在这台电脑或者路由器上的角色——DNS Resolver(也叫递归解析器)。
这个 Resolver 做的第一件事,不是立刻去问别人,而是先看看自己是不是已经知道答案。
因为这不是第一次有人问起类似的域名了。之前查过的结果,很可能还被暂时记着——这就是 DNS 缓存。缓存可能存在于浏览器自己、操作系统,也可能存在于 Resolver 这一层,具体分布因环境而异。
如果缓存里已经有答案,故事到这里就结束了:Resolver 直接把结果交回去,不需要再去问任何人。
但假设这是一次全新的查询,缓存里什么都没有——Resolver 就要真正出去打听了。
第二步:一层一层往下问
这里有一个经常被搞混的地方,需要先说清楚:
是 Resolver 代替浏览器,去逐级查询整个 DNS 体系——而不是浏览器自己依次去问根域名、顶级域、权威服务器。
浏览器从头到尾,只和自己的 Resolver 打交道。真正在 DNS 层级里一步步往下问的,是 Resolver。
这个”往下问”的过程,大致是这样的:
flowchart TD
A[浏览器 / 操作系统] --> B[DNS Resolver]
B --> C{缓存中有答案吗?}
C -- 有 --> H[直接返回结果]
C -- 没有 --> D[询问根域名服务器 Root]
D --> E[根告知: .com 由谁负责]
E --> F[询问 .com 顶级域服务器 TLD]
F --> G[TLD 告知: example.com 的权威服务器是谁]
G --> I[询问 example.com 的权威 DNS]
I --> J[得到最终记录]
J --> HRoot 并不知道 www.example.com 具体对应哪个 IP。它只知道一件事:负责 .com 这个后缀的,是哪一批服务器。
TLD(顶级域)服务器同样不知道最终答案,它只知道:负责 example.com 这个域名的权威服务器是谁。
一直问到 example.com 的权威 DNS 服务器,才终于问到了真正掌握答案的人——它会给出这个域名对应的具体记录。
Resolver 拿到答案后,一边把结果交给浏览器,一边把这个答案暂时记下来——这样下一次再有人问起同一个域名,就不用重新走一遍这套流程了。
记多久,谁说了算?
这里会自然冒出一个问题:缓存的答案,可以一直用下去吗?
答案是不能。每一条 DNS 记录,都带着一个叫 TTL(Time To Live)的数字,单位是秒。它规定了这条记录最多可以被缓存多久。
时间一到,缓存过期,下一次查询就必须重新走一遍上面那套流程,去问一个新的答案——因为地址背后对应的服务器,随时可能发生变化。
一个名字,可能不止一个答案
到这里,数据包本来以为剩下的事情很简单:拿到一个 IP,结束。
但真实情况要更丰富一些。
首先,一个域名可以同时有多种类型的记录:
1 | www.example.com → A 记录 → IPv4 地址 |
A 记录对应 IPv4,AAAA 记录对应 IPv6。同一个域名,两种记录可以同时存在。
其次,DNS 给出的答案,也不一定直接就是最终的 IP。有时候,一个域名会先指向另一个域名,这种记录叫 CNAME:
数据包这才意识到:DNS 并不是一张写死的”域名对应表”,它背后是一整套灵活的记录系统。
亲眼看一眼这个过程
与其只是听人描述,不如自己去看一眼。在终端里执行:
1 | dig www.example.com |
结果里最值得关注的是这一段:
1 |
|
这一行里藏着几件事:记录类型是 A,也就是一个 IPv4 地址;86400 就是这条记录的 TTL,单位是秒;最后的地址,就是这次查询真正要的答案。
如果想分别看 IPv4 和 IPv6:
1 | dig A www.example.com |
如果想亲眼看看前面说的”一层一层往下问”具体是什么样子,可以用:
1 | dig +trace www.example.com |
需要说明一句:这条命令是 dig 特意模拟出的一次完整逐级查询过程,方便观察和调试用的。真实世界里,浏览器每一次访问网站,并不会真的把根域名、顶级域都问一遍——大量查询都在某一层缓存里就已经结束了,只有缓存都没有命中时,才会真的走到这一整套流程。
数据包终于拿到了真正的答案
查询结束了。
1 | www.example.com |
这一次,数据包终于弄明白了自己一直在使用的这个地址,究竟是怎么来的——不是凭空出现的,而是浏览器把问题交给 Resolver,Resolver 或是从缓存里直接给出答案,或是沿着根、顶级域、权威服务器这条链路一层层问出来的。
它低头看了看手里这个地址,长长地舒了一口气。
这一次,终点确实是清楚的了。
但一个熟悉的问题,很快又冒了出来。
它记得,自己曾经因为”只知道 IP 还不够”而卡住过一次——那是因为,知道最终要去哪里,和知道眼前这一步该交给谁,从来都不是同一件事。
现在,它手里的这个 IP 是真实查出来的,不再是一个被直接告知的结果。
可那个问题,会不会又要重新问一遍?
这一跳,我到底应该交给谁?
这一次,它想真正弄清楚这背后发生了什么。
输入一个网址后,DNS 到底做了什么?
https://pengtech.net/network/packet-journey/dns-resolution copy.html